package org.grenader.fileutils;

import java.io.*;
import java.nio.channels.FileChannel;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.logging.Level;

import org.apache.tools.ant.*;

/**
 * <p>This ant task splits a binary file into several files with sizes no more that defined</p>
 *
 * <p>The task generated names of data part files based on the defined prefix and adds "aa", "ab",
 * etc. to ech data part. It works similar to Unix split utility but conseder all files as binary.
 *
 * @author Dale Anson
 * @version $Revision: 157 $
 * @since Ant 1.6
 */
public class SplitTask extends Task {

  private String prefix = "part";
  private long bytes = -1;
  private File file = null;
  private File outputDir = null;
  private boolean failOnError = true;

  /**
   * Constant for compliting file part literal numbers
   */
  private static final int START_LETTER = 65;
  private static final int MAX_LETTER_NUMBER = 26;


  /**
   * <p>Define prefix of file parts. File parts names will contain this given string and a literal
   * file part number.</p> <p>This works similar to Unix split utility naming.</p>
   *
   * @param prefix file part name prefix
   */
  public void setPrefix(String prefix) {
    this.prefix = prefix;
  }

  /**
   * Set size of part in bytes. Note that <b>size</b> parameter has privilege on this <b>bytes</b>
   * parameter.<br> This parameter is not required.
   *
   * @param b number of bytes per part.
   */
  public void setBytes(long b) {
    bytes = b;
  }

  /**
   * Like the linux split command this task allow to define part size in the following way: b for
   * 512, k for 1K, m for 1 Meg. This parameter is not required, but it has privilege on <b>size</b>
   * parameter.<br>
   *
   * @param sizeStr the number of bytes in part, with an optional modifier. If there is no modifier,
   *                given nubmer will be understood as an amount of bytes. For instance,
   *                setSize("10k") works as setBytes(10 * 1024); setSize("2M") worksas setBytes(2 *
   *                1024* 1024)
   */
  public void setSize(String sizeStr) {
    if (sizeStr == null || sizeStr.isEmpty()) {
      return;
    }

    Pattern pattern = Pattern.compile("(\\d+)[.,]?[\\d]?([bkmg]?).*", Pattern.CASE_INSENSITIVE);
    Matcher matcher = pattern.matcher(sizeStr);
    if (!matcher.matches()) {
      throw new BuildException("Invalid size parameter: " + sizeStr + ". " +
          "Valid values are any string of numbers optionally followed by (case insensitive) [b|k|kb|m|mb|g|gb].");
    }

    String digits = matcher.group(1);
    String suffix = matcher.group(2).toLowerCase();
    long number = Long.parseLong(digits);

    if ("k".equalsIgnoreCase(suffix)) {
      number = number * 1024;
    }
    else if ("m".equalsIgnoreCase(suffix)) {
      number = number * 1024 * 1024;
    }
    else if ("g".equalsIgnoreCase(suffix)) {
      number = number * 1024 * 1024 * 1024;
    }
    setBytes(number);
  }

  /**
   * Split the contents of the given file.
   *
   * One of property, value, or file are required.
   *
   * @param file the name of the file
   */
  public void setFile(File file) {
    this.file = file;
  }

  /**
   * Where to put the parts. If file has been set and output directory has not been set, output to
   * directory containing file.
   *
   * @param dir the output directory
   */
  public void setOutputdir(File dir) {
    outputDir = dir;
  }

  /**
   * Determines whether the build should fail if there is an error. Default is true.
   *
   * @param fail true or false
   */
  public void setFailonerror(boolean fail) {
    failOnError = fail;
  }


  /**
   * Execute Ant task. Split given file into pieces with given size and stores them into files with
   * given name prefix.
   *
   * @throws org.apache.tools.ant.BuildException
   *          if failOnError is set to thue
   */
  public void execute() throws BuildException {
    // Check parameters
    if (file == null) throw new BuildException("File property is not defined.");
    if (outputDir == null) throw new BuildException("OutputDir  property is not defined.");

    try {
      splitFile();
    }
    catch (Exception e) {
      if (failOnError) {
        throw new BuildException(e.getMessage());
      }
      else {
        log(e.getMessage());
      }
    }
  }

  /**
   * Splits given file into several files.
   *
   * @throws java.io.IOException if there is an i/o problem
   */
  private void splitFile() throws IOException {
    if (!file.exists()) throw new FileNotFoundException(file.toString());
    if (file.length() == 0) throw new BuildException("Zero length file.");
    if (outputDir == null) outputDir = file.getParentFile();
    if (!outputDir.exists() && !outputDir.mkdirs()) {
      throw new IOException("Unable to create output directory.");
    }

    // Define default bytes value
    if (bytes <= 0) bytes = file.length();

    int suffNum = 0;
    long num_parts = file.length() / bytes;
    long last_part_size = file.length() % bytes;
    boolean one_more = last_part_size > 0;

    FileChannel inChannel = new FileInputStream(file).getChannel();
//    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
    for (int i = 0; i < num_parts; i++) {
      // make files all the same number of bytes
      FileChannel outChannel =
          new FileOutputStream(new File(outputDir, prefix + getSectionSuff(suffNum))).getChannel();
//      FileOutputStream fos = new FileOutputStream(new File(outputDir, prefix + "." + String.valueOf(suffNum)));
      copyToStream(inChannel, outChannel, i * bytes, bytes);
      outChannel.close();
      suffNum++;
    }
    if (one_more) {
      FileChannel outChannel =
          new FileOutputStream(new File(outputDir, prefix + getSectionSuff(suffNum))).getChannel();
//      FileOutputStream fos = new FileOutputStream(new File(outputDir, prefix + "." + String.valueOf(suffNum)));
      copyToStream(inChannel, outChannel, num_parts * bytes, last_part_size);
      outChannel.close();
    }
    inChannel.close();

  }

  /**
   * <p>Generates literal filename suffix based on given file part number</p> <p>The first part will
   * have <b>AA</b> suffix<br> The second part will have <b>AB</b> suffix,<br> ...<br> The 27th part
   * will have <b>BA</b> suffix,<br> etc. </p>
   *
   * @param suffix number of fiel part section
   * @return literal representation of given number
   */
  String getSectionSuff(int suffix) {
    int firstNumber = suffix / MAX_LETTER_NUMBER;
    return new String(Character.toChars(START_LETTER + firstNumber)) +
        new String(Character.toChars(START_LETTER + (suffix % MAX_LETTER_NUMBER)));
  }


  /**
   * Copies data from one {@link java.nio.channels.FileChannel} to another {@link
   * java.nio.channels.FileChannel}
   *
   * @param inChannel  {@link java.nio.channels.FileChannel} to copy data from it
   * @param outChannel {@link java.nio.channels.FileChannel} to copy data into it
   * @param beginFrom  number of bytes in inChannel to start copy data
   * @param size       number of bytes that have to be copied from inChannel to outChannel
   * @return actual number of bytes copied from inChannel to outChannel
   * @throws java.io.IOException on any file error
   */
  private long copyToStream(FileChannel inChannel, FileChannel outChannel, long beginFrom,
                            long size) throws IOException {
    long count = 0;
    try {
      count = inChannel.transferTo(beginFrom, size, outChannel);
    }
    catch (IOException ex) {
      throw ex;
    }
    finally {
      if (outChannel != null) outChannel.close();
    }

    return count;
  }

    public long getBytes() {
    return bytes;
  }
}